/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace zeal.core
{
    public abstract class TextureNode
    {
        private static int _nextId;
        private readonly TextureInputPort[] _inputPorts;
        private readonly TextureOutputPort[] _outputPorts;
        private readonly TextureParameter[] _parameters;
        private readonly TextureNodeInfo _info;
        private readonly TextureGraph _graph;
        private readonly int _id;
        private TexturePixelType? _groupType;

        public double x { get; set; }
        public double y { get; set; }
        public int id { get { return _id; }}

        internal TextureNode(TextureNodeInfo info, TextureGraph graph)
        {
            _info = info;
            _graph = graph;
            _inputPorts = info.inputs.Select(item => new TextureInputPort(item, this)).ToArray();
            _outputPorts = info.outputs.Select(item => new TextureOutputPort(item, this)).ToArray();
            _parameters = info.parameters.Select(item => new TextureParameter(item, this)).ToArray();
            _groupType = hasTypeGroup ? TexturePixelType.VARIABLE : (TexturePixelType?) null;
            _id = _nextId++;
        }

        public TextureInputPort[] inputPorts { get { return _inputPorts; } }
        public TextureOutputPort[] outputPorts { get { return _outputPorts; } }
        public TextureParameter[] parameters { get { return _parameters; } }
        public TextureNodeInfo info { get { return _info; } }
        public TextureGraph graph { get { return _graph; } }
        public TextureInputPort singleInput { get { return _inputPorts.Length == 1 ? _inputPorts[0] : null; } }
        public TextureOutputPort singleOutput { get { return _outputPorts.Length == 1 ? _outputPorts[0] : null; } }
        public IEnumerable<TextureOutputPort> connectedInputs { get { return graph.inputsOf(this); } }
        public IEnumerable<TextureInputPort> connectedOutputs { get { return graph.outputsOf(this); } }
        public bool hasTypeGroup { get { return info.hasTypeGroup; } }
        public TexturePixelType? groupType { get { return _groupType; } }
        public override string ToString() { return info.name; }

        internal void unifyTypeGroup()
        {
            Debug.Assert(hasTypeGroup);

            // First, gather all nodes that are in the same type "group" as this one -- all will have
            // the same type
            HashSet<TextureNode> typeGroup = new HashSet<TextureNode>();
            getTypeGroup(typeGroup);

            // Next, find the unifying type in the type group
            TexturePixelType type = TexturePixelType.VARIABLE;
            foreach(TextureNode node in typeGroup)
            {
                unifyType(node.inputPorts, ref type);
                unifyType(node.outputPorts, ref type);
            }

            // Finally, assign the new type to the nodes
            foreach (TextureNode node in typeGroup)
                node.assignType(type);
        }

        private void getTypeGroup(HashSet<TextureNode> nodes)
        {
            Debug.Assert(hasTypeGroup);
            if(nodes.Add(this))
            {
                foreach (TextureInputPort p in inputPorts)
                    if (p.isVariableType)
                        foreach(TextureOutputPort p2 in p.sources)
                            if(p2.isVariableType)
                                p2.node.getTypeGroup(nodes);
                foreach (TextureOutputPort p in outputPorts)
                    if (p.isVariableType)
                        foreach (TextureInputPort p2 in p.targets)
                            if(p2.isVariableType)
                                p2.node.getTypeGroup(nodes);
            }
        }

        private static void unifyType(TexturePort[] ports, ref TexturePixelType type)
        {
            foreach (TexturePort port in ports)
                if (port.isVariableType)
                    foreach (TexturePort port2 in port.connected)
                    {
                        TexturePixelType? pType = TextureType.match(type, port2.portInfo.type.pixelType);
                        if(!pType.HasValue)
                            throw new InvalidOperationException("Type mismatch!");
                        type = pType.Value;
                    }
        }

        private void assignType(TexturePixelType type)
        {
            if(_groupType != type)
            {
                _groupType = type;
                foreach(TextureInputPort p in _inputPorts)
                    if(p.isVariableType)
                        p.onTypeChanged();
                foreach (TextureOutputPort p in _outputPorts)
                    if (p.isVariableType)
                        p.onTypeChanged();
            }
        }
    }

    public sealed class Tex2DNode : TextureNode
    {
        public static TextureNodeInfo nodeInfo { get { return _info; } }
        private static readonly TextureNodeInfo _info;
        static Tex2DNode()
        {
            TextureInputPortInfo @uv = new TextureInputPortInfo("uv", new TextureType(TextureDataType.PIXEL, TexturePixelType.VECTOR2), true);
            TextureInputPortInfo @in = new TextureInputPortInfo("in", new TextureType(TextureDataType.TEXTURE2D, TexturePixelType.VARIABLE), false);
            TextureOutputPortInfo @out = new TextureOutputPortInfo("out", new TextureType(TextureDataType.PIXEL, TexturePixelType.VARIABLE));
            TextureParameterInfo @texture = new TextureParameterInfo("texture");
            _info = new TextureNodeInfo("Tex2D", typeof(Tex2DNode),
                new TextureInputPortInfo[] { @uv, @in, },
                new TextureOutputPortInfo[] { @out, },
                new TextureParameterInfo[] { @texture, });
        }

        public static TextureNode getInstance(TextureGraph graph) { return new Tex2DNode(graph); }
        private Tex2DNode(TextureGraph graph) : base(_info, graph) { }

        public TextureInputPort @uv { get { return inputPorts[0]; } }
        public TextureInputPort @in { get { return inputPorts[1]; } }
        public TextureOutputPort @out { get { return outputPorts[0]; } }
        public TextureParameter @texture { get { return parameters[0]; } }
    }
}